package db.ibatis.oth;

import db.ibatis.Page;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;

/**
 * 分页拦截器，用于拦截需要进行分页查询的操作，然后对其进行分页处理。
 * 利用拦截器实现Mybatis分页的原理：
 * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象，Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象，
 * 而且对应的Sql语句是在Statement之前产生的，所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手。
 * 在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路
 * 就是拦截住StatementHandler接口的prepare方法，然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句，之后再调用StatementHandler对象
 * 的prepare方法，即调用invocation.proceed()。
 * <p>
 * 对于分页而言，在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少，这是通过获取到了原始的Sql语句后，把它改为对应的统计语句
 * 再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换，之后再执行查询记录数的Sql语句进行总记录数的统计。
 * <p>
 * 对于StatementHandler其实只有两个实现类，一个是RoutingStatementHandler，另一个是抽象类BaseStatementHandler，
 * BaseStatementHandler有三个子类，分别是SimpleStatementHandler，PreparedStatementHandler和CallableStatementHandler，
 * SimpleStatementHandler是用于处理Statement的，PreparedStatementHandler是处理PreparedStatement的，
 * 而CallableStatementHandler是处理CallableStatement的。
 * Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler，而在RoutingStatementHandler里面拥有一个
 * StatementHandler类型的delegate属性，RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler，
 * 即SimpleStatementHandler、PreparedStatementHandler或CallableStatementHandler，在RoutingStatementHandler里面所有
 * StatementHandler接口方法的实现都是调用的delegate对应的方法。
 * 我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法，
 * 又因为Mybatis只有在建立RoutingStatementHandler的时候是通过Interceptor的plugin方法进行包裹的，所以我们这里拦截到的目标对象
 * 肯定是RoutingStatementHandler对象。
 */


@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class PaginationInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(PaginationInterceptor.class);

    private String dialect = "mysql"; //数据库方言
    private String idSuffix = "ByPage"; //mapper.xml中需要拦截的ID(正则匹配)
    private String primitiveSQL = "";
    private Page page = null;


    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler handler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        MappedStatement statement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String mapId = statement.getId();
        if (mapId.matches(".+" + idSuffix)) {
            BoundSql boundSql = handler.getBoundSql();
            Object obj = boundSql.getParameterObject();
            // Get the primitive jdbc sql
            primitiveSQL = boundSql.getSql();
            String countSql = getCountSql();
            Connection conn = (Connection) invocation.getArgs()[0];
            PreparedStatement countStatement = conn.prepareStatement(countSql);
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            parameterHandler.setParameters(countStatement);
            ResultSet rs = countStatement.executeQuery();

            if (rs.next()) {
                long cnt = rs.getLong(1);
                if (cnt > 0) {
                    // Get the parameter from mapper parameter
                    Map<?, ?> param = (Map<?, ?>) boundSql.getParameterObject();
                    page = (Page) param.get("page");
                    if (page != null)
                        page.setTotalNumber((int) cnt);
                } else {
                    return invocation.proceed();
                    //return null;
                }
            }
            // Set the pager sql for mysql
            String pagerSql = getPaggerSql();
            logger.info("执行:" + pagerSql);
            metaObject.setValue("delegate.boundSql.sql", pagerSql);
        }
        return invocation.proceed();
    }

    public Object plugin(Object target) {
        if (target instanceof StatementHandler)
            return Plugin.wrap(target, this);
        return target;
    }

    public void setProperties(Properties properties) {

    }

    private String getCountSql() {
        String cntSQL = "";
        String upperSQL = primitiveSQL.toUpperCase();
        int idx = upperSQL.indexOf("FROM");
        if (dialect.equalsIgnoreCase("mysql")) {
            cntSQL = "SELECT COUNT(1) " + primitiveSQL.substring(idx, primitiveSQL.length());
        }

        return cntSQL;
    }

    private String getPaggerSql() {
        String pgSQL = "";
        if (dialect.equalsIgnoreCase("mysql")) {
            if (this.page != null)
                pgSQL = String.format("%s LIMIT %d , %d", primitiveSQL, page.getDbIndex(), page.getPageSize());
            else
                return primitiveSQL;
        }
        return pgSQL;
    }

}